const REV = 6,
       BRUSHES = ["basic"], //List of available brushes go here
       USER_AGENT = navigator.userAgent.toLowerCase();

var SCREEN_WIDTH = window.innerWidth,
    SCREEN_HEIGHT = window.innerHeight,
    DRAWABLE_WIDTH = 600,
    DRAWABLE_HEIGHT = 800,
    BRUSH_SIZE = 1,
    BRUSH_PRESSURE = 1,
    COLOR = [0, 0, 0],
    BACKGROUND_COLOR = [250, 250, 250],
    STORAGE = window.localStorage,
    brush,
    saveTimeOut,
    wacom,
    i,
    mouseX = 0,
    mouseY = 0,
    container,
    foregroundColorSelector,
    menu,
    about,
    canvas,
    flattenCanvas,
    context,
    isFgColorSelectorVisible = false,
    isBgColorSelectorVisible = false,
    isAboutVisible = false,
    isMenuMouseOver = false,
    shiftKeyIsDown = false,
    altKeyIsDown = false,
    ctrlKeyIsDown = false,
	brushSizeTouchStart = 1,
	brushSizeTouchReference = 0.0,
	interactableUIElements = [];

init();

function init()
{
	var hash, palette, embed, localStorageImage;
	
	if (USER_AGENT.search("android") > -1 || USER_AGENT.search("iphone") > -1)
		BRUSH_SIZE = 2;	
		
	if (USER_AGENT.search("safari") > -1 && USER_AGENT.search("chrome") == -1) // Safari
		STORAGE = false;
	
	document.body.style.backgroundRepeat = 'no-repeat';
	document.body.style.backgroundPosition = 'center center';	
	
	container = document.createElement('div');
	document.body.appendChild(container);

	/*
	 * TODO: In some browsers a nasty "Plugin Missing" window appears and people is getting confused.
	 * Disabling it until a better way to handle it appears.
	 */
	 /*embed = document.createElement('embed');
	 embed.id = 'wacom-plugin';
	 embed.type = 'application/x-wacom-tablet';
	 document.body.appendChild(embed);
	 */
	 
	 //wacom = document.embeds["wacom-plugin"];

	canvas = document.createElement("canvas");
	canvas.width = DRAWABLE_WIDTH;
    canvas.height = DRAWABLE_HEIGHT;
	canvas.id = "mainCanvas";
	canvas.style.cursor = 'crosshair';
	container.appendChild(canvas);
	
	flattenCanvas = document.createElement("canvas");
	flattenCanvas.width = DRAWABLE_WIDTH;
	flattenCanvas.height = DRAWABLE_HEIGHT;
	
	palette = new Palette();
	
    //This is the circular palette that shows up on SHIFT
	foregroundColorSelector = new ColorSelector(palette);
	foregroundColorSelector.addEventListener('change', onForegroundColorSelectorChange, false);
	foregroundColorSelector.container.style.zIndex = 4;
	container.appendChild(foregroundColorSelector.container);
	
	var noScroll = function(event) {event.preventDefault()};
	
	menu = new Menu();
//	menu.container.addEventListener('touchstart', noScroll, false);
	menu.container.addEventListener('touchmove', noScroll, false);
//	menu.container.addEventListener('touchend', noScroll, false);
//	menu.foregroundColor.addEventListener('click', onMenuForegroundColor, false);
//	menu.foregroundColor.addEventListener('touchend', onMenuForegroundColor, false);
	menu.selector.addEventListener('change', onMenuSelectorChange, false);
	menu.save.addEventListener('click', onMenuSave, false);
//	menu.save.addEventListener('touchend', onMenuSave, false);
	menu.exportImage.addEventListener('click', onMenuExportImage, false);
//	menu.exportImage.addEventListener('touchend', onMenuExportImage, false);
	menu.clear.addEventListener('click', onMenuClear, false);
//	menu.bucket.addEventListener('click', onMenuBucket, false);
//	menu.clear.addEventListener('touchend', onMenuClear, false);
//	menu.about.addEventListener('click', onMenuAbout, false);
//	menu.about.addEventListener('touchend', onMenuAbout, false);
	menu.container.addEventListener('mouseover', onMenuMouseOver, false);
	menu.container.addEventListener('mouseout', onMenuMouseOut, false);
	menu.container.style.zIndex = 3;
	container.appendChild(menu.container);
	
	flattenCanvas.height = canvas.height = DRAWABLE_HEIGHT;
	canvas.style.position = 'absolute';
	canvas.style.top = menu.container.offsetHeight+'px';
	context = canvas.getContext("2d");

	if (STORAGE)
	{
		if (localStorage.canvas)
		{
			localStorageImage = new Image();
		
			localStorageImage.addEventListener("load", function(event)
			{
				localStorageImage.removeEventListener(event.type, arguments.callee, false);
				context.drawImage(localStorageImage,0,0);
			}, false);
			
			localStorageImage.src = localStorage.canvas;			
		}
		
		if (localStorage.brush_color_red)
		{
			COLOR[0] = localStorage.brush_color_red;
			COLOR[1] = localStorage.brush_color_green;
			COLOR[2] = localStorage.brush_color_blue;
		}

	}

	foregroundColorSelector.setColor( COLOR );
	
	if (window.location.hash)
	{
		hash = window.location.hash.substr(1,window.location.hash.length);

		for (i = 0; i < BRUSHES.length; i++)
		{
			if (hash == BRUSHES[i])
			{
				brush = eval("new " + BRUSHES[i] + "(context)");
				menu.selector.selectedIndex = i;
				break;
			}
		}
	}

	if (!brush)
	{
		brush = eval("new " + BRUSHES[0] + "(context)");
	}
	
	about = new About();
	container.appendChild(about.container);
	
	window.addEventListener('mousemove', onWindowMouseMove, false);
	window.addEventListener('resize', onWindowResize, false);
	window.addEventListener('keydown', onWindowKeyDown, false);
	window.addEventListener('keyup', onWindowKeyUp, false);
	window.addEventListener('blur', onWindowBlur, false);
	
	document.addEventListener('mousedown', onDocumentMouseDown, false);
	document.addEventListener('mouseout', onDocumentMouseOut, false);
	
	document.addEventListener("dragenter", onDocumentDragEnter, false);  
	document.addEventListener("dragover", onDocumentDragOver, false);
	document.addEventListener("drop", onDocumentDrop, false);  
	
	canvas.addEventListener('mousedown', onCanvasMouseDown, false);
	canvas.addEventListener('touchstart', onCanvasTouchStart, false);

	drawSampleBrush();
	
	onWindowResize(null);
}


// WINDOW

function onWindowMouseMove( event )
{
	mouseX = event.clientX;
	mouseY = event.clientY;
}

function onWindowResize()
{
	//SCREEN_WIDTH = window.innerWidth;
	//SCREEN_HEIGHT = window.innerHeight;
	
	//menu.container.style.left='0px';
	//menu.container.style.width=SCREEN_WIDTH+'px';
	
	//about.container.style.left = ((SCREEN_WIDTH - about.container.offsetWidth) / 2) + 'px';
	//about.container.style.top = ((SCREEN_HEIGHT - about.container.offsetHeight) / 2) + 'px';
}

function onWindowKeyDown( event )
{
	if (shiftKeyIsDown)
		return;
		
	switch(event.keyCode)
	{
		case 16: // Shift
			shiftKeyIsDown = true;
			foregroundColorSelector.container.style.left = mouseX - 125 + 'px';
			foregroundColorSelector.container.style.top = mouseY - 125 + 'px';
			foregroundColorSelector.container.style.visibility = 'visible';
			break;

	    case 17: // Ctrl
	        ctrlKeyIsDown = true;
	        break;

		case 18: // Alt
			altKeyIsDown = true;
			break;
			
		case 68: // [
			if(BRUSH_SIZE > 1) BRUSH_SIZE --;
			break;
		
		case 70: // ]
			BRUSH_SIZE ++;
			break;			
	}
}

function onWindowKeyUp( event )
{
	switch(event.keyCode)
	{
		case 16: // Shift
			shiftKeyIsDown = false;
			foregroundColorSelector.container.style.visibility = 'hidden';			
			break;
			
	    case 17: // Ctrl
	        ctrlKeyIsDown = false;
	        break;

	    case 18: // Alt
			altKeyIsDown = false;
			break;

		case 82: // r
			brush.destroy();
			brush = eval("new " + BRUSHES[menu.selector.selectedIndex] + "(context)");
			break;
		case 66: // b
			document.body.style.backgroundImage = null;
			break;
	}
	
	context.lineCap = BRUSH_SIZE == 1 ? 'butt' : 'round';	
}

function onWindowBlur( event )
{
	shiftKeyIsDown = false;
	altKeyIsDown = false;
}


// DOCUMENT

function isEventInColorSelector(cx, cy) {
	if (!isFgColorSelectorVisible && !isBgColorSelectorVisible) {
		return false;
	}
	
	var xLoc = 0,
		yLoc = 0;
	
	if (isFgColorSelectorVisible) {
		xLoc = foregroundColorSelector.container.offsetLeft + 250;
		yLoc = foregroundColorSelector.container.offsetTop;
	}
	
	xLoc = cx - xLoc;
	yLoc = cy - yLoc;
	
	return (xLoc >= 0 && xLoc <= 150 &&
		    yLoc >= 0 && yLoc <= 250);
}

function onDocumentMouseDown( event )
{
	if (!isMenuMouseOver && !isEventInColorSelector(event.clientX, event.clientY))
	    event.preventDefault();
	var actualX = event.clientX - canvas.offsetLeft;
	var actualY = event.clientY - canvas.offsetLeft;
}

function onDocumentMouseOut( event )
{
	onCanvasMouseUp();
}

function onDocumentDragEnter( event )
{
	event.stopPropagation();
	event.preventDefault();
}

function onDocumentDragOver( event )
{
	event.stopPropagation();
	event.preventDefault();
}

function onDocumentDrop( event )
{
	event.stopPropagation();  
	event.preventDefault();
	
	var file = event.dataTransfer.files[0];
	
	if (file.type.match(/image.*/))
	{
		/*
		 * TODO: This seems to work on Chromium. But not on Firefox.
		 * Better wait for proper FileAPI?
		 */

		var fileString = event.dataTransfer.getData('text').split("\n");
		document.body.style.backgroundImage = 'url(' + fileString[0] + ')';
	}	
}


// COLOR SELECTORS

function onForegroundColorSelectorChange( event )
{
	COLOR = foregroundColorSelector.getColor();
	
	menu.setForegroundColor( COLOR );

	if (STORAGE)
	{
		localStorage.brush_color_red = COLOR[0];
		localStorage.brush_color_green = COLOR[1];
		localStorage.brush_color_blue = COLOR[2];		
	}
}


// MENU

function onMenuForegroundColor()
{
	cleanPopUps();
	
	foregroundColorSelector.show();
	foregroundColorSelector.container.style.left = ((SCREEN_WIDTH - foregroundColorSelector.container.offsetWidth) / 2) + 'px';
	foregroundColorSelector.container.style.top = ((SCREEN_HEIGHT - foregroundColorSelector.container.offsetHeight) / 2) + 'px';

	isFgColorSelectorVisible = true;
}

function onMenuSelectorChange()
{
	if (BRUSHES[menu.selector.selectedIndex] == "")
		return;

	brush.destroy();
	brush = eval("new " + BRUSHES[menu.selector.selectedIndex] + "(context)");

	window.location.hash = BRUSHES[menu.selector.selectedIndex];
}

function onMenuMouseOver()
{
	isMenuMouseOver = true;
}

function onMenuMouseOut()
{
	isMenuMouseOver = false;
}

function onMenuSave()
{
	saveToLocalStorage();
}

function onMenuExportImage()
{
	// window.open(canvas.toDataURL('image/png'),'mywindow');
	flatten();
	//window.open(flattenCanvas.toDataURL('image/png'),'mywindow');
    $('#imageinput').val(flattenCanvas.toDataURL('image/png'));
   // var thumbnailContext = $('#thumbnailCanvas')[0].getContext('2d');
    //thumbnailContext.drawImage(flattenCanvas, 0, 0, flattenCanvas.width, flattenCanvas.height, 0, 0, 120, 160);
    //$('#thumbnailinput').val(thumbnailCanvas.toDataURL('image/png'));
    $('#thumbnailinput').val(downScaleCanvas(flattenCanvas, 0.2).toDataURL('image/png'));
    $('#imageSubmit').submit();
}

function onMenuClear()
{
	if (!confirm("Are you sure?"))
		return;
		
	context.clearRect(0, 0, DRAWABLE_WIDTH, DRAWABLE_HEIGHT);

	saveToLocalStorage();

	brush.destroy();
	brush = eval("new " + BRUSHES[menu.selector.selectedIndex] + "(context)");
}

function onMenuAbout()
{
	cleanPopUps();

	isAboutVisible = true;
	about.show();
}


// CANVAS

function onCanvasMouseDown( event )
{
	var data, position;

	clearTimeout(saveTimeOut);
	cleanPopUps();
	
	if (altKeyIsDown)
	{
		flatten();
		
		data = flattenCanvas.getContext("2d").getImageData(0, 0, flattenCanvas.width, flattenCanvas.height).data;
		position = (event.clientX + (event.clientY * canvas.width)) * 4;
		
		foregroundColorSelector.setColor( [ data[position], data[position + 1], data[position + 2] ] );
		
		return;
	}

	if(ctrlKeyIsDown)
	{
	    flood(event.clientX, event.clientY);
	    return;
	}
	
	BRUSH_PRESSURE = wacom && wacom.isWacom ? wacom.pressure : 1;
	
	brush.strokeStart( event.clientX - canvas.offsetLeft, event.clientY - canvas.offsetTop );

	window.addEventListener('mousemove', onCanvasMouseMove, false);
	window.addEventListener('mouseup', onCanvasMouseUp, false);
    //The brush might leave the canvas; We need to account for that so we can resume the stroke
	$('#mainCanvas').on("mouseleave", onCanvasMouseLeave); 
	
}

function onCanvasMouseMove( event )
{
	BRUSH_PRESSURE = wacom && wacom.isWacom ? wacom.pressure : 1;
	
	brush.stroke( event.clientX - canvas.offsetLeft, event.clientY - canvas.offsetTop );
}

function onCanvasMouseUp()
{
    brush.strokeEnd();

    for (index = 0; index < interactableUIElements.length; ++index) {
        interactableUIElements[index].style.pointerEvents = 'auto';
        console.log("interactibleUIElements ENABLED");
    }
	
	window.removeEventListener('mousemove', onCanvasMouseMove, false);
	window.removeEventListener('mouseup', onCanvasMouseUp, false);
	$('#mainCanvas').off("mouseleave"); //Brush no longer has risk of leaving the canvas bounds
	
	if (STORAGE)
	{
		clearTimeout(saveTimeOut);
		saveTimeOut = setTimeout(saveToLocalStorage, 2000, true);
	}
}


function showFGColorPickerAtLocation(loc) {
	foregroundColorSelector.show();
	foregroundColorSelector.container.style.left = (loc[0] - (foregroundColorSelector.container.offsetWidth / 2)) + 'px';
	foregroundColorSelector.container.style.top = (loc[1] - (foregroundColorSelector.container.offsetHeight / 2)) + 'px';

	isFgColorSelectorVisible = true;
}

function averageTouchPositions(touches) {
	var touchLength = touches.length;
	var average = [0,0];
	
	for (var i = 0; i < event.touches.length; ++i) {
		var touch = event.touches[i];
		average[0] += touch.pageX;
		average[1] += touch.pageY;
	}
	average[0] = average[0] / touches.length;
	average[1] = average[1] / touches.length;
	
	return average;
}

function distance(a, b) {
	var dx=a.pageX-b.pageX;
	var dy=a.pageY-b.pageY;
	return Math.sqrt(dx*dx + dy*dy);
}

function onCanvasTouchStart( event )
{
	clearTimeout(saveTimeOut);
	cleanPopUps();		

	if(event.touches.length == 1)
	{
		// draw
		event.preventDefault();
		
		brush.strokeStart( event.touches[0].pageX - canvas.offsetLeft, event.touches[0].pageY - canvas.offsetTop );
		
		window.addEventListener('touchmove', onCanvasTouchMove, false);
		window.addEventListener('touchend', onCanvasTouchEnd, false);
	}
	else if (event.touches.length == 2)
	{
		// brush size
		event.preventDefault();
		
		brushSizeTouchReference = distance(event.touches[0], event.touches[1]);
		brushSizeTouchStart = BRUSH_SIZE;
		
		window.addEventListener('touchmove', onBrushSizeTouchMove, false);
		window.addEventListener('touchend', onBrushSizeTouchEnd, false);
	}
	else if (event.touches.length == 3)
	{
		// foreground color
		event.preventDefault();
		
		var loc = averageTouchPositions(event.touches);
		showFGColorPickerAtLocation(loc);
		
		window.addEventListener('touchmove', onFGColorPickerTouchMove, false);
		window.addEventListener('touchend', onFGColorPickerTouchEnd, false);
	}
	else if (event.touches.length == 4)
	{
		// reset brush
		event.preventDefault();
		window.addEventListener('touchend', onResetBrushTouchEnd, false);
	}
}

function onCanvasTouchMove( event )
{
	if(event.touches.length == 1)
	{
		event.preventDefault();
		brush.stroke( event.touches[0].pageX - canvas.offsetLeft, event.touches[0].pageY - canvas.offsetTop );
	}
}

function onCanvasTouchEnd( event )
{
	if(event.touches.length == 0)
	{
		event.preventDefault();
		
		brush.strokeEnd();

		window.removeEventListener('touchmove', onCanvasTouchMove, false);
		window.removeEventListener('touchend', onCanvasTouchEnd, false);
		
		if (STORAGE)
		{
			clearTimeout(saveTimeOut);
			saveTimeOut = setTimeout(saveToLocalStorage, 2000, true);
		}
	}
}

function onResetBrushTouchEnd( event )
{
	if (event.touches.length == 0)
	{
		event.preventDefault();
		brush.destroy();
		brush = eval("new " + BRUSHES[menu.selector.selectedIndex] + "(context)");
		window.removeEventListener('touchend', onResetBrushTouchEnd, false);
		
		if (STORAGE)
		{
			clearTimeout(saveTimeOut);
			saveTimeOut = setTimeout(saveToLocalStorage, 2000, true);
		}
	}
}

function onFGColorPickerTouchMove( event )
{
	if (event.touches.length == 3)
	{
		event.preventDefault();
		var loc = averageTouchPositions(event.touches);
		foregroundColorSelector.container.style.left = (loc[0] - (foregroundColorSelector.container.offsetWidth / 2)) + 'px';
		foregroundColorSelector.container.style.top = (loc[1] - (foregroundColorSelector.container.offsetHeight / 2)) + 'px';
	}
}

function onFGColorPickerTouchEnd( event )
{
	if (event.touches.length == 0)
	{
		event.preventDefault();
		
		window.removeEventListener('touchmove', onFGColorPickerTouchMove, false);
		window.removeEventListener('touchend', onFGColorPickerTouchEnd, false);
		
		if (STORAGE)
		{
			clearTimeout(saveTimeOut);
			saveTimeOut = setTimeout(saveToLocalStorage, 2000, true);
		}
	}
}

function onBrushSizeTouchMove( event )
{
	if (event.touches.length == 2)
	{
		event.preventDefault();
		
		var size = brushSizeTouchStart + (distance(event.touches[0], event.touches[1]) - brushSizeTouchReference) / 4;
		BRUSH_SIZE = Math.max(Math.min(Math.floor(size), 320), 1);
	}
}

function onBrushSizeTouchEnd( event )
{
	if (event.touches.length == 0)
	{
		event.preventDefault();
		
		window.removeEventListener('touchmove', onBrushSizeTouchMove, false);
		window.removeEventListener('touchend', onBrushSizeTouchEnd, false);
		
		if (STORAGE)
		{
			clearTimeout(saveTimeOut);
			saveTimeOut = setTimeout(saveToLocalStorage, 2000, true);
		}
	}
}

//

function saveToLocalStorage()
{
	localStorage.canvas = canvas.toDataURL('image/png');
}

function flatten()
{
	var context = flattenCanvas.getContext("2d");
	
	context.fillStyle = 'rgb(255,255,255)';
	context.fillRect(0, 0, canvas.width, canvas.height);
	context.drawImage(canvas, 0, 0);
}

function cleanPopUps()
{
	if (isFgColorSelectorVisible)
	{
		foregroundColorSelector.hide();
		isFgColorSelectorVisible = false;
	}
	
	if (isAboutVisible)
	{
		about.hide();
		isAboutVisible = false;
	}
}

//Takes a hexCode string without the #, should allow # later
function eSetColor(hexCode)
{
    //hexCode = hexCode.substring(hexCode.length - 6, hexCode.length - 1);
    COLOR[0] = parseInt((hexCode).substring(0, 2), 16);
    COLOR[1] = parseInt((hexCode).substring(2, 4), 16);
    COLOR[2] = parseInt((hexCode).substring(4, 6), 16);
    drawSampleBrush();
}

//Takes an int
function eSetSize(size)
{
    BRUSH_SIZE = size;
    drawSampleBrush();
}

function drawSampleBrush()
{
    //windowContext.clearRect(25, 725, 50, 50);

    //windowContext.beginPath();
    //windowContext.fillStyle = "rgba(255,255,255,1)";
    //windowContext.strokeStyle = "#000000";
    //windowContext.lineWidth = 2;
    //windowContext.rect(25, 725, 50, 50);
    //windowContext.closePath();
    //windowContext.fill();
    //windowContext.stroke();

    //windowContext.fillStyle = "rgba(" + COLOR[0] + ", " + COLOR[1] + ", " + COLOR[2] + ", " + context.globalAlpha + ")";
    //windowContext.beginPath();
    //windowContext.arc(50, 750, BRUSH_SIZE / 2, 0, Math.PI * 2, true);
    //windowContext.closePath();
    //windowContext.fill();
}

function eSetAlpha(alpha)
{
    context.globalAlpha = alpha;
    drawSampleBrush();
}

function continueStroke(event)
{
    endContinueStroke();
    onCanvasMouseDown(event); //Initiate a stroke
}

function onCanvasMouseLeave(event)
{
    $('#mainCanvas').off("mouseleave"); //Mouse has left already. Unbind.
    $('#mainCanvas').on("mouseenter", continueStroke); //Mouse will re-enter the canvas, and brush stroke must continue. Bind event.
    document.addEventListener('mouseup', endContinueStroke, false); //If user stops stroke outside of canvas, don't continue the stroke when they return
}

function endContinueStroke(event)
{
    $('#mainCanvas').off("mouseenter");
}


// scales the canvas by (float) scale < 1
// returns a new canvas containing the scaled image.
// Written by http://stackoverflow.com/users/856501/gamealchemist
// From http://stackoverflow.com/questions/18922880/html5-canvas-resize-downscale-image-high-quality
function downScaleCanvas(cv, scale) {
    if (!(scale < 1) || !(scale > 0)) throw ('scale must be a positive number <1 ');
    var sqScale = scale * scale; // square scale = area of source pixel within target
    var sw = cv.width; // source image width
    var sh = cv.height; // source image height
    var tw = Math.ceil(sw * scale); // target image width
    var th = Math.ceil(sh * scale); // target image height
    var sx = 0, sy = 0, sIndex = 0; // source x,y, index within source array
    var tx = 0, ty = 0, yIndex = 0, tIndex = 0; // target x,y, x,y index within target array
    var tX = 0, tY = 0; // rounded tx, ty
    var w = 0, nw = 0, wx = 0, nwx = 0, wy = 0, nwy = 0; // weight / next weight x / y
    // weight is weight of current source point within target.
    // next weight is weight of current source point within next target's point.
    var crossX = false; // does scaled px cross its current px right border ?
    var crossY = false; // does scaled px cross its current px bottom border ?
    var sBuffer = cv.getContext('2d').
    getImageData(0, 0, sw, sh).data; // source buffer 8 bit rgba
    var tBuffer = new Float32Array(4 * sw * sh); // target buffer Float32 rgb
    var sR = 0, sG = 0,  sB = 0; // source's current point r,g,b
    // untested !
    var sA = 0;  //source alpha    

    for (sy = 0; sy < sh; sy++) {
        ty = sy * scale; // y src position within target
        tY = 0 | ty;     // rounded : target pixel's y
        yIndex = 4 * tY * tw;  // line index within target array
        crossY = (tY != (0 | ty + scale)); 
        if (crossY) { // if pixel is crossing botton target pixel
            wy = (tY + 1 - ty); // weight of point within target pixel
            nwy = (ty + scale - tY - 1); // ... within y+1 target pixel
        }
        for (sx = 0; sx < sw; sx++, sIndex += 4) {
            tx = sx * scale; // x src position within target
            tX = 0 |  tx;    // rounded : target pixel's x
            tIndex = yIndex + tX * 4; // target pixel index within target array
            crossX = (tX != (0 | tx + scale));
            if (crossX) { // if pixel is crossing target pixel's right
                wx = (tX + 1 - tx); // weight of point within target pixel
                nwx = (tx + scale - tX - 1); // ... within x+1 target pixel
            }
            sR = sBuffer[sIndex    ];   // retrieving r,g,b for curr src px.
            sG = sBuffer[sIndex + 1];
            sB = sBuffer[sIndex + 2];
            sA = sBuffer[sIndex + 3];
            
            if (!crossX && !crossY) { // pixel does not cross
                // just add components weighted by squared scale.
                tBuffer[tIndex    ] += sR * sqScale;
                tBuffer[tIndex + 1] += sG * sqScale;
                tBuffer[tIndex + 2] += sB * sqScale;
                tBuffer[tIndex + 3] += sA * sqScale;
            } else if (crossX && !crossY) { // cross on X only
                w = wx * scale;
                // add weighted component for current px
                tBuffer[tIndex    ] += sR * w;
                tBuffer[tIndex + 1] += sG * w;
                tBuffer[tIndex + 2] += sB * w;
                tBuffer[tIndex + 3] += sA * w;
                // add weighted component for next (tX+1) px                
                nw = nwx * scale
                tBuffer[tIndex + 4] += sR * nw; // not 3
                tBuffer[tIndex + 5] += sG * nw; // not 4
                tBuffer[tIndex + 6] += sB * nw; // not 5
                tBuffer[tIndex + 7] += sA * nw; // not 6
            } else if (crossY && !crossX) { // cross on Y only
                w = wy * scale;
                // add weighted component for current px
                tBuffer[tIndex    ] += sR * w;
                tBuffer[tIndex + 1] += sG * w;
                tBuffer[tIndex + 2] += sB * w;
                tBuffer[tIndex + 3] += sA * w;
                // add weighted component for next (tY+1) px                
                nw = nwy * scale
                tBuffer[tIndex + 4 * tw    ] += sR * nw; // *4, not 3
                tBuffer[tIndex + 4 * tw + 1] += sG * nw; // *4, not 3
                tBuffer[tIndex + 4 * tw + 2] += sB * nw; // *4, not 3
                tBuffer[tIndex + 4 * tw + 3] += sA * nw; // *4, not 3
            } else { // crosses both x and y : four target points involved
                // add weighted component for current px
                w = wx * wy;
                tBuffer[tIndex    ] += sR * w;
                tBuffer[tIndex + 1] += sG * w;
                tBuffer[tIndex + 2] += sB * w;
                tBuffer[tIndex + 3] += sA * w;
                // for tX + 1; tY px
                nw = nwx * wy;
                tBuffer[tIndex + 4] += sR * nw; // same for x
                tBuffer[tIndex + 5] += sG * nw;
                tBuffer[tIndex + 6] += sB * nw;
                tBuffer[tIndex + 7] += sA * nw;
                // for tX ; tY + 1 px
                nw = wx * nwy;
                tBuffer[tIndex + 4 * tw    ] += sR * nw; // same for mul
                tBuffer[tIndex + 4 * tw + 1] += sG * nw;
                tBuffer[tIndex + 4 * tw + 2] += sB * nw;
                tBuffer[tIndex + 4 * tw + 3] += sA * nw;
                // for tX + 1 ; tY +1 px
                nw = nwx * nwy;
                tBuffer[tIndex + 4 * tw + 4] += sR * nw; // same for both x and y
                tBuffer[tIndex + 4 * tw + 5] += sG * nw;
                tBuffer[tIndex + 4 * tw + 6] += sB * nw;
                tBuffer[tIndex + 4 * tw + 7] += sA * nw;
            }
        } // end for sx 
    } // end for sy

    // create result canvas
    var resCV = document.createElement('canvas');
    resCV.width = tw;
    resCV.height = th;
    var resCtx = resCV.getContext('2d');
    var imgRes = resCtx.getImageData(0, 0, tw, th);
    var tByteBuffer = imgRes.data;
    // convert float32 array into a UInt8Clamped Array
    var pxIndex = 0; //  
    for (sIndex = 0, tIndex = 0; pxIndex < tw * th; sIndex += 4, tIndex += 4, pxIndex++) {
        tByteBuffer[tIndex] = Math.ceil(tBuffer[sIndex]);
        tByteBuffer[tIndex + 1] = Math.ceil(tBuffer[sIndex + 1]);
        tByteBuffer[tIndex + 2] = Math.ceil(tBuffer[sIndex + 2]);
        tByteBuffer[tIndex + 3] = Math.ceil(tBuffer[sIndex + 3]);
    }
    // writing result to canvas.
    resCtx.putImageData(imgRes, 0, 0);
    return resCV;
}